﻿/// ===============================================================================
/// Copyright (C) 2011  Ron Cresswell
/// ===============================================================================
/// This program is free software; you can redistribute it and/or
/// modify it under the terms of the GNU General Public License
/// as published by the Free Software Foundation; either version 2
/// of the License, or (at your option) any later version.
/// 
/// This program is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
/// GNU General Public License for more details.
/// 
/// You should have received a copy of the GNU General Public License
/// along with this program; if not, write to the Free Software
/// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
/// ===============================================================================
/// 
/// Project:        SharePoint Filtered Search
/// Author:         Ron Cresswell (ron.cresswell@angrymansoftware.com.au)
/// Company:        Angry Man Software
/// Date:           18/07/2011 (18 July 2011)  
/// Version:        0.97 (Beta)
///
/// ===============================================================================
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Administration;
using Microsoft.Office.Server.Search;
using Microsoft.Office.Server.Search.Query;
using Microsoft.Office.Server.Search.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Utilities;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;


namespace AngryManSoftware.WebParts
{
    public enum tableStyles
    {
        Style1,
        Style2,
        Style3,
        Plain
    }

    [Guid("4D88860C-B0A9-471a-B3B0-5C561C6E9833")]
    public class AMFilteredSearch : System.Web.UI.WebControls.WebParts.WebPart
    {
        /*
         * A web part to perform "ad hoc" full-text searches limiting results to documents
         * where given column values match, without requiring those columns to be indexed.
         * 
         * Hence this allows searching within document "types", even when the meta-data
         * that defines that document type has only been set up for one library on an ad hoc basis.
         * 
         * Note - I use the terms "field" and "column" interchangeably. Sorry.
         * 
         * 
         * Objects:
         * declare them here,
         * instantiate them in CreateChildControls
         * set their values in OnPreRender
         *
         * Version 0.9 - some variables instantiated at the same time as declared, some not
         *
         * Version 0.91 - assume limit on number of columns (5), and declare all
         *  objects in class, instantiate in CreateChildControls (note - may not know # columns 
         *  at this point) and populate in "OnPreRender"
         * 
         * Version 0.94 - fixed bug where the limit on no. of unique column values
         *  was ignoring the user setting
         *  - Now sorts the column values alphabetically (with user definable option to reverse sort)
         *  - Added option to modify the library link to filter the library page by the relvant column values
         *  
         * Version 0.97 - Added functionality to detect if we are installed on a SharePoint 2010 server and 
         * adjust the search accordingly
         */        

        #region object/variable declarations
        Literal brk;
        Literal spc;
        Literal mLiteral; // any error or warning message (mainly for debug)
        Label label_info; // copyright message or setup message & link 
        Label label_summary; // search results summary
        Label label_results; // search results
        SPGridView resultsGrid; //search results (grid view)
        Label SearchLabel;
        Literal brk2;
        CheckBoxList[] cbl_array;
        //ListBox[] lbl_array;
        HtmlSelect[] lbl_array;
        TextBox textBox_SearchTerm;
        Button SearchButton;
        TextBox textBox_Debug;
        Table myTable;
        Literal table_start;
        Literal l_styleBlock;
        HtmlHead head;
        Color myColor;
        AMStyle myStyle;
        Literal[] tableColSeparators;
        List<string> libShortUrls;
        int numColumns;
        string searchTerm;
        string[] columnNames;
        //globals used in SearchButton_Click
        bool[] noneSelected;
        bool nothingSelected;

        
        #endregion

        #region persistent property declarations
        private string color_theme = "#86B3EE";
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public string ColorTheme
        {
            get { return color_theme; }
            set { color_theme = value; }
        }
        private Hashtable fieldTypes;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable Field_Types
        {
            get { return fieldTypes; }
            set { fieldTypes = value; }
        }
        private tableStyles _Style;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public tableStyles Table_Style
        {
            get { return _Style; }
            set { _Style = value; }
        }
        
        private string _message;
        public string Message
        {
            get { return _message; }
            set { _message = value; }
        }

        private string debug_text;
        public string DebugText
        {
            get { return debug_text; }
            set { debug_text = value; }
        }
        //new editor functions:
        // 1. Reverse alphabetical sort (Z->A)
        private bool reverse_alpha = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ReverseAlpha
        {
            get { return reverse_alpha; }
            set { reverse_alpha = value; }
        }
        // 2. filter library link
        private bool filter_library_links = true;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool FilterLibraryLink
        {
            get { return filter_library_links; }
            set { filter_library_links = value; }
        }
        // 3. Results display grid option
        private bool show_results_in_grid = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ResultGrid
        {
            get { return show_results_in_grid; }
            set { show_results_in_grid = value; }
        }
        // 4. Turn on/off display of matching columns
        private bool show_matching_columns = true;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ShowMatchingColumns
        {
            get { return show_matching_columns; }
            set { show_matching_columns = value; }
        }
        // 5. Turn on/off display of search hits in columns
        private bool show_hits_in_columns = true;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ShowHitsInColumns
        {
            get { return show_hits_in_columns; }
            set { show_hits_in_columns = value; }
        }
        // 6. Option to display search box at top rather than below column choices
        private bool search_box_above = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool SearchBoxAbove
        {
            get { return search_box_above; }
            set { search_box_above = value; }
        }
        // 7. Turn on/off display of column values used in searching, whether or not they contain a search hit
        private bool show_search_columns = true;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ShowSearchColumns
        {
            get { return show_search_columns; }
            set { show_search_columns = value; }
        }
        //
        private bool show_library_links = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ShowLibraryLink
        {
            get { return show_library_links; }
            set { show_library_links = value; }
        }
        //
        private bool use_list_boxes = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool UseListBoxes
        {
            get { return use_list_boxes; }
            set { use_list_boxes = value; }
        }
        private int max_vals = 10;
        [WebBrowsable(false)]
        [Personalizable]
        public int MaxVals
        {
            get { return max_vals; }
            set { max_vals = value; }
        }
        private int search_rtn_limit = 200;
        [WebBrowsable(false)]
        [Personalizable]
        public int SearchReturnLimit
        {
            get { return search_rtn_limit; }
            set { search_rtn_limit = value; }
        }
        //
        // Libraries
        //
        private Hashtable am_libs_avail; // List of all libraries
        [WebBrowsable(false)] // ie editor part to be a custom part, not using propertygrid part.
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMLibsAvail
        {
            get { return am_libs_avail; }
            set { am_libs_avail = value; }
        }
        private Hashtable am_libs_sel; // List of selected libraries
        [WebBrowsable(false)] // ie editor part to be a custom part, not using propertygrid part.
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMLibsSel
        {
            get { return am_libs_sel; }
            set
            {
                am_libs_sel = value;
                try
                {
                    populateLibShortUrls();
                }
                catch (Exception ex)
                {
                    _message = ex.Message;
                }
            }
        }
       

        //
        // Columns
        //
        private Hashtable am_cols_avail; // List of selected libraries
        [WebBrowsable(false)] // ie editor part to be a custom part, not using propertygrid part.
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMColsAvail
        {
            get { return am_cols_avail; }
            set { am_cols_avail = value; }
        }
        private Hashtable am_cols_sel; // List of selected libraries
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMColsSel
        {
            get { return am_cols_sel; }
            set { am_cols_sel = value; }
        }
        private Hashtable col_values; // key = Column Name, value = List<string>
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMColVals
        {
            get { return col_values; }
            set { col_values = value; }
        }
        private bool show_debug = false;
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public bool ShowDebug
        {
            get { return show_debug; }
            set { show_debug = value; }
        }
        //
        // Columns to be searched
        //
        private Hashtable am_search_cols_avail; // List of available columns for searching metadata
        [WebBrowsable(false)] 
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMSearchColsAvail
        {
            get { return am_search_cols_avail; }
            set { am_search_cols_avail = value; }
        }
        private Hashtable am_search_cols_sel; // List of selected columns for searching
        [WebBrowsable(false)]
        [Personalizable(PersonalizationScope.Shared)]
        public Hashtable AMSearchColsSel
        {
            get { return am_search_cols_sel; }
            set { am_search_cols_sel = value; }
        }
        

        #endregion

        #region constructor
        /// <summary>
        /// Constructor - initialise variables
        /// </summary>
        public AMFilteredSearch()
        {
            am_libs_sel = new Hashtable();
            am_libs_avail = new Hashtable();
            am_cols_sel = new Hashtable();
            am_cols_avail = new Hashtable();
            am_search_cols_sel = new Hashtable();
            am_search_cols_avail = new Hashtable();
            col_values = new Hashtable();
            SearchButton = new Button();
            textBox_SearchTerm = new TextBox();
            searchTerm = "";
            fieldTypes = new Hashtable();
            libShortUrls = new List<string>();
        }
        #endregion

        #region main methods

        protected override void OnLoad(EventArgs e)
        {            
            base.OnLoad(e);
            //clear any error message on page load:
            _message = "";
            /*
             * Note - at present (29/10/2010) the routine "UpdateColumnValues" clears and then
             * recreates the child controls. For some reason, this creates a button object with
             * the wrong click handler reference and so the search button click throws an unhandled
             * and untrappable error.
             * 
             * Need to change "UpdateColumnValues" to only refresh the hashtables. If the controls need 
             * to be recreated, that should be a separate call.
             */
            //check available column values on page load:
            //try
            //{
                //UpdateColumnValues();
            //}
            //catch (Exception ex)
            //{
            //    _message = _message + "<br>" + ex.Message;
            //}
        }
        protected override void CreateChildControls()
        {
            brk = new Literal();
            spc = new Literal();
            mLiteral = new Literal();
            textBox_Debug = new TextBox();
            textBox_Debug.TextMode = TextBoxMode.MultiLine;
            Unit w = new Unit(80, UnitType.Em);
            Unit h = new Unit(20, UnitType.Em);
            textBox_Debug.Width = w;
            textBox_Debug.Height = h;
            textBox_Debug.Wrap = true;
            if (show_debug)
            {
                textBox_Debug.Visible = true;
            }
            else textBox_Debug.Visible = false;
            label_summary = new Label();
            label_results = new Label();
            resultsGrid = new SPGridView();
            resultsGrid.AutoGenerateColumns = false;
            brk2 = new Literal();
            textBox_SearchTerm = new TextBox();
            SearchButton = new Button();
            myTable = new Table();
            table_start = new Literal();
            base.CreateChildControls();
            head = this.Page.Header;
            l_styleBlock = new Literal();
            
            if ((am_libs_sel.Count == 0) || (am_cols_sel.Count == 0))
            {
                //nothing set up - suggest going to modify menu...
                label_info = new Label();
                this.Controls.Add(label_info);                
            }
            else
            { // main work here
                this.Controls.Add(brk);
                StringBuilder tablebuilder = new StringBuilder();
                if (AMColsSel.Count != AMColVals.Count)
                {
                    UpdateColumnValues();
                }
                if (AMColsSel.Count != AMColVals.Count)
                {
                    throw new Exception("Error - failed to correctly set column values");
                }
                SearchButton.Click += new EventHandler(SearchButton_Click);
                SearchLabel = new Label();
                if (search_box_above)
                {                    
                    this.Controls.Add(SearchButton);
                    this.Controls.Add(spc);
                    this.Controls.Add(textBox_SearchTerm);
                    this.Controls.Add(SearchLabel);                    
                }
                numColumns = AMColVals.Count;
                columnNames = new string[AMColsSel.Count];
                this.Controls.Add(table_start);
                tableColSeparators = new Literal[numColumns];
                //IF using checkboxes:-
                if (!UseListBoxes)
                {
                    cbl_array = new CheckBoxList[numColumns];                    
                    for (int ic = 0; ic < numColumns; ic++)
                    {
                        cbl_array[ic] = new CheckBoxList();
                        this.Controls.Add(cbl_array[ic]);
                        tableColSeparators[ic] = new Literal();
                        this.Controls.Add(tableColSeparators[ic]);
                    }
                }
                //OR if using listboxes:-
                else
                {
                    //lbl_array = new ListBox[numColumns];
                    lbl_array = new HtmlSelect[numColumns];
                    for (int ic = 0; ic < numColumns; ic++)
                    {
                        //lbl_array[ic] = new ListBox();
                        //lbl_array[ic].Rows = 10;
                        //lbl_array[ic].SelectionMode = ListSelectionMode.Multiple;
                        lbl_array[ic] = new HtmlSelect();
                        lbl_array[ic].Size = 10;
                        lbl_array[ic].Multiple = true;
                        this.Controls.Add(lbl_array[ic]);
                        tableColSeparators[ic] = new Literal();
                        this.Controls.Add(tableColSeparators[ic]);
                    }
                }
                //
                if (!search_box_above)
                {
                    this.Controls.Add(textBox_SearchTerm);                    
                    this.Controls.Add(SearchLabel);
                    this.Controls.Add(SearchButton);
                }
                this.Controls.Add(label_summary);
                this.Controls.Add(mLiteral);
                this.Controls.Add(brk2);
                this.Controls.Add(textBox_Debug);
                this.Controls.Add(label_results);
                this.Controls.Add(resultsGrid);
                label_info = new Label();
                this.Controls.Add(label_info);              
            }
            
        }

        protected override void OnPreRender(EventArgs e)
        {
            /*
             * In here. the controls have all been created and added to the list of controls
             * on the page. However, this method is not simply called when the user, in the 
             * EditorPart, clicks "Apply" or "OK". It also gets called if the user does something
             * that causes a postback to the server. This could be adding or removing a column,
             * or, potentially, drifting the mouse over a nav control (although I tried this and it 
             * didn't, so perhaps we were wrong about those navigation controls causing a postback)
             * 
             * In any case, the values in the controls need to be "set" here, *not* added to. This
             * is particularly important for checkBoxLists. Choice of solution is whether to clear the lists
             * then reconstruct them, or to test for the number of items to see if they have been
             * already filled.
             * 
             * The disadvantage to clearing the checkBoxList items and rebuilding them, is that the
             * page cannot remember the selected boxes, which the user would not expect to "clear"
             * when he/she hits the search button.
             * 
             * However, the implication is that it may not be so bad to set the values of the controls in
             * CreateChildControls after all - as long as those controls are defined within the web part
             * class and only instantiated in the CreateChildControls method.
             * 
             */
            base.OnPreRender(e);
            try
            {
                bool hasAMStyle = false;
                try
                {
                    if (head.HasControls())
                    {
                        foreach (Control ctrl in head.Controls)
                        {
                            string cid = ctrl.ID;
                            if (cid == "amStyleBlock")
                            {
                                head.Controls.Remove(ctrl);
                                break;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error removing CSS from header: error message is:" + ex.Message, ex.InnerException);
                }
                try
                {
                    Version buildVersion = SPFarm.Local.BuildVersion;
                    int M0 = buildVersion.Major;
                    int MR = buildVersion.MajorRevision;
                    int m0 = buildVersion.Minor;
                    int mr = buildVersion.MinorRevision;
                    string version = buildVersion.ToString();
                    if (mr > 6415) // don't bother if the current version is older than April SP2
                    {
                        
                        if (head.HasControls())
                        {
                            foreach (Control ctrl in head.Controls)
                            {
                                string cid = ctrl.ID;
                                if (cid == "amQueryScriptBlock")
                                {
                                    head.Controls.Remove(ctrl);
                                    break;
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error removing Query Script block from header: error message is:" + ex.Message, ex.InnerException);
                }
                try
                {
                    if (!hasAMStyle)
                    {
                        /*
                         * Style1 = coloured background, table & header with borders
                         * Style2 = coloured background, table & header items with borders
                         * Style3 = coloured background, table with no border, header with underline
                         * Plain = empty style. No colour, no grid lines.
                         */
                        if (color_theme == "")
                        {
                            color_theme = "#86B3EE";
                        }
                        myColor = parseColor(color_theme);
                        myStyle = new AMStyle(myColor);
                        if (myStyle.Message != "")
                        {
                            _message = myStyle.Message;
                        }
                        l_styleBlock.Text = myStyle.getStyles();
                        l_styleBlock.ID = "amStyleBlock";
                        head.Controls.Add(l_styleBlock);
                    }
                }
                catch(Exception ex)
                {
                    throw new Exception("Error inserting CSS in header: error message is:" + ex.Message, ex.InnerException);
                }
                try
                {
                    if ((am_libs_sel.Count == 0) || (am_cols_sel.Count == 0))
                    {
                        string myID = this.ID;
                        label_info.Text = "<span style=\"width:100%; color: #FF0000; text-align: center\">"
                        + "<br><br>Web Part not yet configured.<br>Please open \"Modify Shared Web Part\" menu<br>to select libraries to search and columns to choose from<br><br>"
                        + "<br>"
                        + "Click <A HREF=javascript:MSOTlPn_ShowToolPane2Wrapper('Edit',this,'"
                        + myID
                        + "');>here</A> to open the web part editor<br><br>"
                        + "</span>"
                        + "<br>"
                        + "&copy 2009 Ron Cresswell, <A HREF='http://www.angrymansoftware.com.au'>Angry Man Software</A><br>";
                    }
                    else
                    {
                        spc.Text = "&nbsp;";
                        string tstyle = "am" + Table_Style.ToString().ToLower();
                        string lbstyle = "amlb" + Table_Style.ToString().ToLower();
                        brk.Text = "<br>";
                        StringBuilder tablebuilder = new StringBuilder();
                        if (numColumns > 0)
                        {
                            int count = 0;
                            foreach (string key in AMColsSel.Keys)
                            {
                                string val = (string)AMColsSel[key];
                                int i = -1;
                                bool rtn = Int32.TryParse(val.Substring(val.LastIndexOf('_') + 1), out i);
                                if (rtn)
                                {
                                    if (i < AMColsSel.Count)
                                    {
                                        columnNames[i] = key;
                                        count++;
                                    }
                                }
                            }
                            if (count != AMColsSel.Keys.Count)
                            {
                                throw new Exception("Error: indexing of columns failed");
                            }
                            tablebuilder.AppendLine("<br><TABLE class=\"" + tstyle + "\"><TR>");
                            for (int i = 0; i < count; i++)
                            {
                                tablebuilder.AppendLine("<TH>&nbsp" + columnNames[i] + "&nbsp</TH>");
                            }
                            tablebuilder.AppendLine("</TR><TR valign=\"top\"><TD>&nbsp");

                            table_start.Text = tablebuilder.ToString();

                            for (int i = 0; i < count; i++)
                            {
                                string key = columnNames[i];
                                if (AMColVals[key] is List<string>)
                                {
                                    List<string> vals = AMColVals[key] as List<string>;
                                    int max = vals.Count;
                                    if (max > MaxVals)
                                    {
                                        max = MaxVals;
                                    }
                                    if (!UseListBoxes)
                                    {
                                        if (cbl_array[i].Items.Count == 0) // fill only if empty (allow preserving check state)
                                        {
                                            for (int j = 0; j < max; j++)
                                            {
                                                cbl_array[i].Items.Add(vals[j]);
                                            }
                                        }
                                        cbl_array[i].CssClass = tstyle;
                                        cbl_array[i].BorderWidth = new Unit(0);
                                    }
                                    else // ListBoxes:-
                                    {
                                        if (lbl_array[i].Items.Count == 0)
                                        {
                                            for (int j = 0; j < max; j++)
                                            {
                                                string value = j + "_" + vals[j];
                                                ListItem li = new ListItem(vals[j], value);
                                                lbl_array[i].Items.Add(li);                                                
                                            }
                                        }
                                        //TODO - Need some CSS that renders the textboxes nicely.
                                        //lbl_array[i].CssClass = tstyle;
                                        lbl_array[i].Attributes.Add("class",lbstyle);
                                    }
                                }
                                else
                                {
                                    throw new Exception("Error - hashtable seems to contain wrong type of object");
                                }

                                if (i < numColumns - 1)
                                {
                                    tableColSeparators[i].Text = "&nbsp</TD><TD>&nbsp";
                                }
                                else
                                {
                                    tableColSeparators[i].Text = "&nbsp</TD></TR></TABLE><BR>";
                                }
                            }
                        }
                        textBox_SearchTerm.Text = searchTerm;
                        Unit len = new Unit(200);
                        textBox_SearchTerm.Width = len;
                        SearchButton.Text = "Search";
                        SearchLabel.Text = " &larr; Enter search term for full-text search <br><br>";
                        brk2.Text = "<br>";
                        label_info.Text = "<br>";
                    }
                }
                catch (Exception ex)
                {
                    throw new Exception("Error in OnPreRender, msg is " + ex.Message, ex.InnerException);
                }
                //
                // Display error/warning message if any
                //
                if (_message != "")
                {
                    mLiteral.Text = "<br><span style=\"width:70%; color: #FF0000; text-align: center\">" + _message + "</span><br>";
                }
                if (show_debug)
                {
                    textBox_Debug.Text = debug_text;
                }
            }
            catch (Exception ex)
            {
                label_info.ForeColor = Color.Red;
                if (ex.InnerException != null)
                {
                    label_info.Text = ex.Message + "<br>" + ex.InnerException.ToString();
                }
                else
                {
                    label_info.Text = ex.Message;
                }
                if (_message != "")
                {
                    mLiteral.Text = "<br><span style=\"width:100%; color: #FF0000; text-align: center\">" + _message + "</span><br>";
                }
            }
        }

        void SearchButton_Click(object sender, EventArgs e)
        {
            /*
             * Process is as follows:
             * 
             * 1. Look through all checkbox lists (one per selected column) and collect
             *    list of column values to match against. 
             * 2. Look through all selected libraries and collate all list items (documents) that
             *    match the selected criteria.
             * 3. Perform search and only display results where the document returned matches our 
             *    selections.
             * 
             * TODO:-
             * 
             * 1. Need to paginate results.
             * 2. Need to preserve search term. [done]
             * 3. Implement "OR" between column values, but "AND" between columns [done]
             * 4. Sort results table by Rank [done]
             * 5. Implement additional search using only the selected metadata
             * 5A:- Need to ensure that hits common to *both* searches are presented first
             *      Note also that Rank *may* take care of this for us. [done]
             * 
             */
            if (sender is Button)
            {
                try
                {
                    #region define local variables/objects
                    debug_text = ""; // clear the text box
                    label_summary.Text = "<hr><br>";
                    label_results.ForeColor = Color.Black;
                    StringBuilder myQuery = new StringBuilder();
                    string item = "<br><A href='{0}' />{1}</A> <BR>{2}<BR>";
                    StringBuilder sb = new StringBuilder();
                    List<string> matching_ItemUrls = new List<string>();
                    List<string> matching_ItemTitles = new List<string>();
                    List<List<string>> colVals = new List<List<string>>();
                    //Hashtable itemMatches = new Hashtable(); // key = itemUrl (lower case); object = List<string[]>
                    Hashtable itemMatches = new Hashtable();// key = itemUrl (lower case); object = AMSearchResult.
                    #endregion
                    //
                    //Check to see if anything is selected:-
                    //
                    //
                    #region set up noneSelected (boolean per column) and "nothingSelected" (boolean)
                    #region (a) for checkBoxes
                    if (!UseListBoxes)
                    {
                        noneSelected = new bool[cbl_array.Length];
                        nothingSelected = true;
                        for (int i = 0; i < cbl_array.Length; i++)
                        {
                            noneSelected[i] = true;
                            for (int j = 0; j < cbl_array[i].Items.Count; j++)
                            {
                                if (cbl_array[i].Items[j].Selected)
                                {
                                    noneSelected[i] = false;
                                    nothingSelected = false;
                                    break;
                                }
                            }
                        }
                    }
                    #endregion
                    #region (b) for listBoxes
                    else
                    {
                        noneSelected = new bool[lbl_array.Length];
                        nothingSelected = true;
                        for (int i = 0; i < lbl_array.Length; i++)
                        {
                            noneSelected[i] = true;
                            for (int j = 0; j < lbl_array[i].Items.Count; j++)
                            {
                                if (lbl_array[i].Items[j].Selected)
                                {
                                    noneSelected[i] = false;
                                    nothingSelected = false;
                                    break;
                                }
                            }
                        }
                    }
                    #endregion
                    #endregion                    
                    //
                    #region calc the required # matches:
                    //
                    int nReqd = 0;
                    for (int k = 0; k < AMColsSel.Count; k++)
                    {
                        if (!noneSelected[k]) nReqd++;
                    }
                    #endregion
                    //
                    #region set up column values to be matched for each column (--> List<List<string>> colVals)
                        // (a) for checkBoxes:
                    if (!UseListBoxes)
                    {
                        for (int i = 0; i < cbl_array.Length; i++)
                        {
                            colVals.Add(new List<string>());
                            for (int j = 0; j < cbl_array[i].Items.Count; j++)
                            {
                                if (cbl_array[i].Items[j].Selected)
                                {
                                    colVals[i].Add(cbl_array[i].Items[j].Text);
                                }
                            }
                        }
                    }
                        // (b) for listBoxes:
                    else
                    {
                        for (int i = 0; i < lbl_array.Length; i++)
                        {
                            colVals.Add(new List<string>());
                            for (int j = 0; j < lbl_array[i].Items.Count; j++)
                            {
                                if (lbl_array[i].Items[j].Selected)
                                {
                                    colVals[i].Add(lbl_array[i].Items[j].Text);
                                }
                            }
                        }
                    }
                    #endregion
                    #region put selected column names into string[] columnNames, in order determined by position in editor list box
                    foreach (string key in AMColsSel.Keys)
                    {
                        //check that the value has an "_<list box posn>" appended:
                        string val = (string)AMColsSel[key];
                        //bool is the return val from a TryParse. "i" contains the position
                        //of the item in the list box
                        int i = -1;
                        bool rtn = Int32.TryParse(val.Substring(val.LastIndexOf('_') + 1), out i);
                        if (rtn)
                        {
                            if (i < AMColsSel.Count)
                            {
                                columnNames[i] = key;
                            }
                        }
                    }
                    #endregion
                    //
                    // Now have the required column names in "columnNames", and in the right order (so?)
                    //
                    // already have the searchColumns in AMSearchColsSel.Keys
                    //
                    #region loop over each selected list and find docs that match the criteria
                    SPWeb thisWeb = SPContext.Current.Web;
                    foreach (string key in AMLibsSel.Keys) // loop over each selected list
                    {
                        Guid libGuid = new Guid((string)AMLibsSel[key]);
                        SPList aList = getListCurrentUser(libGuid);
                        //
                        // TODO - ensure that search column values are added here as part of the AMSearchResult
                        // that is entered into the itemMatches hashtable - shouldn't need to access the SPList or SPWeb
                        // objects more than once.
                        //
                        if (aList != null) // don't consider it if we don't have access to it
                        {
                            #region gather the fields from columnNames that are used by this list (--> "fields")
                            string lib = "<A href='" + aList.DefaultViewUrl + "' />" + aList.Title + "</A>";
                            List<string> fields = new List<string>();
                            for (int i = 0; i < columnNames.Length; i++)
                            {
                                string fieldKey = columnNames[i];

                                if (aList.Fields.ContainsField(fieldKey))
                                {
                                    fields.Add(fieldKey);
                                }
                            }
                            #endregion
                            #region loop through list items looking for matches
                            if (fields.Count > 0) // don't bother if this list has none of the fields we need!
                            {
                                //
                                // Now loop through all items in the list selecting the ones that match the filter columns:
                                //
                                foreach (SPListItem anItem in aList.Items)
                                {                                    
                                    string url = anItem.Url.ToLower();
                                    List<AMMatch> matches = new List<AMMatch>();
                                    #region find all matches for this item (--> List<AMMatch> matches)
                                    for (int i = 0; i < fields.Count; i++)
                                    {
                                        #region "vals" contains lower case versions of all column values to match against
                                        List<string> vals = new List<string>();
                                        for (int j = 0; j < columnNames.Length; j++)
                                        {
                                            if (columnNames[j] == fields[i])
                                            {
                                                //MOD for case insensitivity:
                                                for (int ival = 0; ival < colVals[j].Count; ival++)
                                                {
                                                    vals.Add(colVals[j][ival].ToLower());
                                                }
                                            }
                                        }
                                        #endregion
                                        #region if item[field] contains the correct value, create match and add to "matches"
                                        AMMatch match = new AMMatch();
                                        //
                                        if (anItem[fields[i]] != null)
                                        {
                                            int ftype = (int)fieldTypes[fields[i]];
                                            //MOD for case insensitivity:
                                            string val = Field2String(fields[i], ftype, anItem).ToLower();
                                            if (vals.Contains(val))
                                            {
                                                match.FieldName = fields[i];
                                                match.FieldValue = val;
                                                match.ParentLibraryName = aList.Title;
                                                match.ParentDefaultViewURL = aList.DefaultViewUrl;
                                                match.FieldInternalName = anItem.Fields.GetField(fields[i]).InternalName;
                                                match.FieldHtmlValue = SPEncode.HtmlEncode(val);
                                                matches.Add(match);
                                            }
                                        }
                                        #endregion
                                    }
                                    #endregion
                                    // TODO - noted 10/11/10
                                    // Instead of 3 seperate List<string> objects, these should be a single class.
                                    //
                                    if (matches.Count == nReqd || nothingSelected)
                                    {
                                        AMSearchResult sr = new AMSearchResult();
                                        #region check for search column values
                                        foreach (string scol in AMSearchColsSel.Keys)
                                        {
                                            if (aList.Fields.ContainsField(scol))
                                            {
                                                if (anItem[scol] != null)
                                                {
                                                    if (!sr.SearchColumnValues.ContainsKey(scol))
                                                    {
                                                        sr.SearchColumnValues[scol] = anItem[scol];
                                                    }
                                                }
                                            }
                                        }
                                        #endregion                                        
                                        sr.FileName = anItem.Name;
                                        sr.Matches = matches;
                                        // The list Url should be one that can be used to 
                                        // retrieve the list via "SPWeb.GetList(url)" method
                                        sr.ParentLib = thisWeb.Url + "/" + aList.RootFolder.Url;
                                        sr.Url = thisWeb.Url.ToLower() + "/" + url.ToLower();
                                        matching_ItemUrls.Add(url); // lower case Url
                                        matching_ItemTitles.Add(anItem.Name);
                                        if (nothingSelected)
                                        {
                                            AMMatch pseudomatch = new AMMatch();
                                            pseudomatch.FieldName = "<nofield>";
                                            pseudomatch.FieldValue = "<noval>";
                                            pseudomatch.ParentDefaultViewURL = aList.DefaultViewUrl;
                                            pseudomatch.ParentLibraryName = aList.Title;
                                            matches.Add(pseudomatch);
                                        }
                                        sr.Matches = matches;
                                        //
                                        // MOD - about to change this from a hashtable lookup between the item url and
                                        // a List<AMMatch> object, to a hashtable lookup between the item url and an 
                                        // AMSearchResult object.
                                        //
                                        itemMatches[url] = sr;
                                    }
                                }
                            }
                            #endregion
                        }
                    }
                    #endregion
                    
                    //
                    // Now do the search...
                    //                   
                    bool useSearch = true;
                    string query = textBox_SearchTerm.Text; // this is now the search term
                    searchTerm = query;
                    if (query == "")
                    {
                        useSearch = false; // just return all matches
                    }

                    if (useSearch)
                    {
                        #region setup for search
                        List<AMSearchResult> resultList = new List<AMSearchResult>();
                        //
                        // Store results in a hashtable where key=document url and object=AMSearchResult
                        // - should be faster for checking if a document has already been found.
                        //
                        // However, still need to use a generic List object as well so that we can sort
                        // the results.
                        //
                        
                        
                        string here = thisWeb.ServerRelativeUrl;
                        string weburl = thisWeb.Url;
                        string httpchopped = weburl.Substring(7);
                        string serverchopped = httpchopped.Substring(httpchopped.IndexOf('/') + 1).ToLower() + "/";
                        FullTextSqlQuery myq = new FullTextSqlQuery(SPContext.Current.Site);
                        KeywordQuery mykwq = new KeywordQuery(SPContext.Current.Site)
                        {
                            ResultTypes = ResultType.RelevantResults
                        };
                        #endregion
                        
                        #region search in selected search columns - new logic
                        //
                        // In the new logic, there is no need for the list of AMMatches for hits in search columns
                        //
                        // itemMatches is a hashtable where the key is the item url and the object is an AMSearchResult
                        //
                        foreach (string url in itemMatches.Keys)
                        {
                            AMSearchResult sRes = itemMatches[url] as AMSearchResult;
                            sRes.Rank = 0;
                            foreach (string scol in sRes.SearchColumnValues.Keys)
                            {
                                if (sRes.SearchColumnValues[scol].ToString().ToLower().Contains(query.ToLower()))
                                {
                                    sRes.Rank += 1000;
                                }
                            }
                            resultList.Add(sRes);
                        }
                        #endregion
                        #region execute the search
                        myQuery.Append("SELECT url, filename, HitHighlightedSummary, rank FROM SCOPE() WHERE isDocument=1 AND \"scope\"='All Sites' ");
                        myQuery.Append("AND contains(url, '" + here + "') AND FREETEXT('" + query + "')");
                        myq.QueryText = myQuery.ToString();
                        myq.ResultTypes = ResultType.RelevantResults;
                        myq.EnableStemming = false;
                        myq.TrimDuplicates = false;
                        myq.RowLimit = search_rtn_limit;

                        string q = query + " AND (Path:\"" + here + "*\")";
                        mykwq.QueryText = q;
                        mykwq.SelectProperties.Add("Title");
                        mykwq.SelectProperties.Add("Path");
                        mykwq.SelectProperties.Add("HitHighlightedSummary");
                        mykwq.SelectProperties.Add("Filename");
                        mykwq.SelectProperties.Add("Rank");
                        mykwq.SelectProperties.Add("ContentClass");
                        mykwq.SelectProperties.Add("IsDocument");
                        mykwq.SelectProperties.Add("SiteName");


                        ResultTableCollection rtc;
                        string whereAmI = SPUtility.GetGenericSetupPath(@"TEMPLATE\LAYOUTS");

                        bool mustBe2010 = false;
                        if (whereAmI.Contains(@"\Web Server Extensions\14\"))
                        {
                            mustBe2010 = true;
                        }
                        if (!mustBe2010)
                        {
                            rtc = myq.Execute();
                        }
                        else
                        {
                            rtc = mykwq.Execute();
                        }

                        ResultTable resultTable = rtc[ResultType.RelevantResults];

                        int rtc_count = resultTable.RowCount;
                        if (rtc_count == search_rtn_limit)
                        {
                            _message = _message + "<br>Note: Search Return limit reached. You may be missing some search hits<br>";
                        }
                        /*
                         * Current BUG - "Property doesn't exist or is used in a manner inconsistent with schema settings."
                         * 
                         * This seems to happen if the managed properties contain an entry for a column with the same
                         * name as the user-defined column here. A choice field seems ok as long as it doesn't clash
                         * with a managed property, even though we're using the internal name, which must be unique!
                         * 
                         */
                        #endregion
                        #region read search results into listResults, if it's not already there
                        if (rtc_count > 0)
                        {
                            while (resultTable.Read())
                            {
                                AMSearchResult sResult = new AMSearchResult();
                                //columns in resultTable in the order they appear in sql query 
                                sResult.Url = resultTable.GetString(0).ToLower();
                                sResult.FileName = resultTable.GetString(1);
                                sResult.HitHighlightedSummary = resultTable.GetString(2);                                
                                int rank;
                                bool getRank = Int32.TryParse(resultTable.GetString(3), out rank);
                                if (getRank)
                                {
                                    sResult.Rank = rank;
                                }
                                else
                                {
                                    sResult.Rank = 0;
                                }
                                if (mustBe2010)
                                {
                                    sResult.Url = resultTable.GetString(1).ToLower();
                                    sResult.FileName = resultTable.GetString(3);
                                    sResult.HitHighlightedSummary = resultTable.GetString(2);
                                    Int64 longRank = resultTable.GetInt64(4);
                                    rank = (Int32)longRank;

                                    sResult.Rank = rank;

                                    string Title = resultTable.GetString(0).ToLower();
                                    string ContentClass = resultTable.GetString(5).ToLower();
                                    bool IsDoc = resultTable.GetBoolean(6);
                                    string SiteName = resultTable.GetString(7).ToLower();
                                }
                                AMSearchResult s = resultList.Find(delegate(AMSearchResult sr) { return sr.Url == sResult.Url; });
                                if (s != null) // only consider results in our list
                                {                                    
                                    s.Rank = s.Rank + sResult.Rank; // higher number is a better hit.
                                    s.HitHighlightedSummary = sResult.HitHighlightedSummary;
                                    if (s.FileName != sResult.FileName)
                                    {
                                        throw new Exception("Error - filenames appear to differ. <br>Existing fname=" + s.FileName + "<br>while search result filename=" + sResult.FileName + "<br>");
                                    }
                                }
                            }
                        }
                        AMSortSearchResult ams = new AMSortSearchResult();
                        resultList.Sort(ams);
                        #endregion
                        #region write resultList to debug text box
                        if (show_debug)
                        {
                            StringBuilder debug_entry = new StringBuilder();
                            foreach (AMSearchResult sr in resultList)
                            {
                                debug_entry.AppendLine("*******************");
                                debug_entry.AppendLine(sr.ToString());
                            }
                            debug_text += debug_entry.ToString();
                        }
                        #endregion
                        #region write summary information
                        string results_header = "<b>Results</b>";
                        if (nothingSelected)
                        {
                            results_header += " (no selections made - assuming no filtering)";
                        }
                        label_summary.Text = "<br><hr><br>" + results_header + "<br><br>";
                        if (rtc_count == 0)
                        {
                            label_results.Text = "No documents found";
                        }
                        #endregion
                        #region write out results, if there are any
                        else
                        {
                            if (!show_results_in_grid)
                            {
                                int match_count = 0;
                                int non_match_count = 0;
                                bool libMatch = false;
                                foreach (AMSearchResult sResult in resultList)
                                {
                                    if (sResult.Rank > 0)
                                    {
                                        string foundText = sResult.HitHighlightedSummary;
                                        string highlighted = sResult.FormattedHitHighlight(query);
                                        if (highlighted == "") highlighted = foundText;
                                        string url = sResult.Url.ToLower();
                                        string rurl = url.Substring(url.IndexOf(serverchopped) + serverchopped.Length);
                                        string lurl = rurl.Substring(0, rurl.IndexOf('/'));
                                        string ranking = sResult.Rank.ToString();
                                        if (libShortUrls.Contains(lurl))
                                        {
                                            libMatch = true;
                                        }
                                        else
                                        {
                                            libMatch = false;
                                        }
                                        //
                                        // itemMatches contains all docs (by url) whose columns match the
                                        // user selected filter columns. It's a hashtable, key=url of item
                                        // (lower case), object = AMSearchResult object
                                        //
                                        if (itemMatches.ContainsKey(rurl))
                                        {
                                            //List<AMMatch> matches = itemMatches[rurl] as List<AMMatch>;
                                            AMSearchResult sr = itemMatches[rurl] as AMSearchResult;
                                            List<AMMatch> matches = sr.Matches;
                                            if (matches.Count == nReqd || nothingSelected)
                                            {
                                                sb.AppendFormat(item, sResult.Url,
                                                  sResult.FileName, highlighted);
                                                string matched_cols = "";
                                                string filter = "?";
                                                string hits_in_columns = "";
                                                int k = 0;
                                                for (int i = 0; i < AMColsSel.Count; i++)
                                                {
                                                    if (!noneSelected[i])
                                                    {
                                                        matched_cols += "<b>" + matches[k].FieldName + "</b> : " + matches[k].FieldValue + "<br>";
                                                        int kp1 = k + 1; // because filter field numbering starts at 1
                                                        if (k > 0) filter += "&";
                                                        filter += "FilterField" + kp1 + "=" + matches[k].FieldInternalName + "&FilterValue" + kp1 + "=" + matches[k].FieldHtmlValue;
                                                        k++;
                                                    }
                                                }
                                                //
                                                // TODO: sResult.SearchColumnMatches should contain the value of the search column
                                                // if "show_search_columns" is true, regardless of whether it's a search hit.
                                                // 
                                                // However, if it *is* a search hit, it should also increase ranking at the
                                                // same time.
                                                //
                                                foreach (string key in sResult.SearchColumnValues.Keys)
                                                {
                                                    string val = sResult.SearchColumnValues[key].ToString();
                                                    string lcVal = val.ToLower();
                                                    string lcQuery = query.ToLower();
                                                    string highlightedColumnVal = "";
                                                    if (show_search_columns)
                                                    {
                                                        highlightedColumnVal = val + "<br>";
                                                    }
                                                    if (lcVal.Contains(lcQuery))
                                                    {
                                                        int spos = val.IndexOf(query, StringComparison.CurrentCultureIgnoreCase);
                                                        int lenValue = val.Length;
                                                        int lenQuery = query.Length;
                                                        string p1 = val.Substring(0, spos);
                                                        string p2 = val.Substring(spos, lenQuery);
                                                        string p3 = val.Substring(spos + lenQuery, lenValue - (lenQuery + spos));
                                                        highlightedColumnVal = p1 + "<b>" + p2 + "</b>" + p3 + "<br>";
                                                    }
                                                    if (highlightedColumnVal != "")
                                                    {
                                                        hits_in_columns += key + ": " + highlightedColumnVal;
                                                    }
                                                    else
                                                    {
                                                        hits_in_columns += "";
                                                    }
                                                }                                                

                                                //sb.Append("Rank: <b>" + ranking + "</b><br>");
                                                if (ShowLibraryLink)
                                                {
                                                    if (FilterLibraryLink & !nothingSelected)
                                                    {
                                                        sb.Append("In library <A href='" + matches[0].ParentDefaultViewURL + filter + "'>" + matches[0].ParentLibraryName + "</A><br>");
                                                    }
                                                    else
                                                    {
                                                        sb.Append("In library <A href='" + matches[0].ParentDefaultViewURL + "'>" + matches[0].ParentLibraryName + "</A><br>");
                                                    }
                                                }
                                                if (show_matching_columns)
                                                {
                                                    sb.Append(matched_cols);
                                                }
                                                if (show_hits_in_columns || show_search_columns) 
                                                {
                                                    //need to test whether hit is in column & display nought unless "show_search_columns" is true
                                                    sb.Append(hits_in_columns);
                                                }
                                                match_count++;
                                            }
                                            else
                                            {
                                                non_match_count++;
                                            }
                                        }
                                        else if (libMatch)
                                        {
                                            non_match_count++;
                                        }
                                    }
                                }
                                string otherlibs = "";
                                if (rtc_count > (match_count + non_match_count))
                                {
                                    int others = rtc_count - (match_count + non_match_count);
                                    otherlibs = " (" + others + " in libraries other than those specified in settings)";
                                }
                                string baseText = rtc_count + " Documents found";
                                if (rtc_count == 1)
                                {
                                    baseText = "One document found";
                                }
                                else if (rtc_count == 0)
                                {
                                    baseText = "No documents found";
                                }
                                string joinText = " of which " + match_count + " matched";
                                if (rtc_count == match_count)
                                {
                                    joinText = " all of which matched";
                                    if (rtc_count == 1)
                                    {
                                        joinText = " which matched";
                                    }
                                }
                                if (match_count == 0)
                                {
                                    joinText = " none of which matched";
                                    if (rtc_count == 1)
                                    {
                                        joinText = " which did not match";
                                    }
                                }
                                label_summary.Text += baseText + " on this site" + otherlibs + "," + joinText + " your selections<br>";
                                label_results.Text = sb.ToString();
                            }
                            else
                            {
                                DataTable table = new DataTable();
                                int nRows = table.Rows.Count;
                                int nCols = table.Columns.Count;
                                string url="";
                                string rurl="";
                                string lurl="";
                                string foundText="";
                                string highlighted="";
                                for (int irow = 0; irow < nRows; irow++)
                                {
                                    DataRow dr = table.Rows[irow];
                                    object[] itemArray = dr.ItemArray;
                                    for (int icol=0;icol<nCols;icol++)
                                    {
                                        DataColumn column = table.Columns[icol];

                                        string header = column.ColumnName;
                                        if (header == "URL")
                                        {
                                            url = itemArray[icol].ToString().ToLower();
                                            rurl = url.Substring(url.IndexOf(serverchopped) + serverchopped.Length);
                                            lurl = rurl.Substring(0, rurl.IndexOf('/'));
                                        }
                                        else if (header == "FILENAME")
                                        {
                                        }
                                        else if (header == "HITHIGHLIGHTEDSUMMARY")
                                        {
                                             foundText = itemArray[icol].ToString();
                                             highlighted = highlightText(foundText, query);
                                        }
                                        else if (header == "RANK")
                                        {
                                        }
                                        
                                        //string foundText = resultTable.GetString(2);
                                        //string test0 = resultTable.GetString(0);
                                        //string test1 = resultTable.GetString(1);
                                        //string rank = resultTable.GetString(3);
                                        //string highlighted = highlightText(foundText, query);
                                        
                                    }
                                }
                                foreach (DataColumn column in table.Columns)
                                {
                                    SPBoundField field = new SPBoundField();
                                    field.HeaderText = column.ColumnName;
                                    field.DataField = column.ColumnName;
                                    resultsGrid.Columns.Add(field);
                                }
                                resultsGrid.DataSource = table;
                                resultsGrid.DataBind();
                                resultsGrid.Height=20;
                                resultsGrid.AllowFiltering=true;
                            }
                        }
                        #endregion
                    }
                    else // just list matching documents
                    {
                        #region some columns are selected
                        if (!nothingSelected)
                        {
                            string docitem = "<br><br><A href='{0}' />{1}</A><br>";
                            label_summary.Text = "<hr><br>No search string entered - returning all matching documents<br>";
                            foreach (string rurl in itemMatches.Keys)
                            {
                                //string rurl = matching_ItemUrls[i];
                                if (!(itemMatches[rurl] is AMSearchResult))
                                {                                    
                                    throw new Exception("Error - matching_ItemUrls is the wrong type!!");
                                }
                                AMSearchResult sr = itemMatches[rurl] as AMSearchResult;
                                if (show_debug)
                                {
                                    StringBuilder debug_entry = new StringBuilder();
                                    debug_entry.AppendLine("*******************");
                                    debug_entry.AppendLine(sr.ToString());
                                    debug_text += debug_entry.ToString();
                                }
                                List<AMMatch> matches = sr.Matches;
                                //List<AMMatch> matches = itemMatches[rurl] as List<AMMatch>;
                                int nCount = 0;
                                if (matches != null) nCount = matches.Count;
                                if (nCount == nReqd)
                                {
                                    string here = thisWeb.Url;
                                    if (here[here.Length - 1] != '/')
                                    {
                                        here += "/";
                                    }
                                    string testUrl = sr.Url;
                                    string testListUrl = sr.ParentLib;
                                    //string itemurl = here + matching_ItemUrls[i];
                                    string itemurl = here + sr.Url;
                                    string itemTitle = sr.FileName;
                                    sb.AppendFormat(docitem, itemurl,
                                                 itemTitle);
                                    string matched_cols = "";
                                    string filter = "?";
                                    int kf = 0;
                                    for (int ic = 0; ic < AMColsSel.Count; ic++)
                                    {
                                        if (!noneSelected[ic])
                                        {
                                            matched_cols += "<b>" + matches[kf].FieldName + "</b> : " + matches[kf].FieldValue + "<br>";
                                            int kp1 = kf + 1; // because filter field numbering starts at 1
                                            if (kf > 0) filter += "&";
                                            filter += "FilterField" + kp1 + "=" + matches[kf].FieldInternalName + "&FilterValue" + kp1 + "=" + matches[kf].FieldHtmlValue;
                                            kf++;
                                        }
                                    }
                                    if (ShowLibraryLink)
                                    {
                                        if (FilterLibraryLink & !nothingSelected)
                                        {
                                            sb.Append("In library <A href='" + matches[0].ParentDefaultViewURL + filter + "'>" + matches[0].ParentLibraryName + "</A><br>");
                                        }
                                        else
                                        {
                                            sb.Append("In library <A href='" + matches[0].ParentDefaultViewURL + "'>" + matches[0].ParentLibraryName + "</A><br>");
                                        }
                                    }
                                    sb.Append(matched_cols);
                                    if (show_search_columns)
                                    {
                                        foreach (string scol in sr.SearchColumnValues.Keys)
                                        {
                                            string val = sr.SearchColumnValues[scol] as string;
                                            sb.Append(scol + ": " + val + "<br>");
                                        }
                                    }
                                }
                            }
                            label_results.Text = sb.ToString();
                        }
                        #endregion
                        else
                        {
                            label_summary.Text = "<hr><br>No columns selected and no search term given - nothing to do<br>";
                            label_results.Text = "";
                        }
                    }
                    Label endLabel = new Label();
                    endLabel.Text = "<BR><hr><BR>";
                    this.Controls.Add(endLabel);
                }
                catch (Exception ex)
                {
                    label_results.ForeColor = Color.Red;
                    string msg = "SearchButton_Click: " + ex.Message + "<br>";
                    if (ex.Source != null) msg = msg + "Source: " + ex.Source + "<br>";
                    if (ex.StackTrace != null) msg = msg + "StackTrace: " + ex.StackTrace + "<br>";
                    if (ex.InnerException != null)
                    {
                        Exception x = ex.InnerException;
                        msg = msg + "InnerException:<br>" + x.Message + "<br>";
                        if (x.Source != null) msg = msg + "Source: " + x.Source + "<br>";
                        if (x.StackTrace != null) msg = msg + "StackTrace: " + x.StackTrace + "<br>";
                    }
                    label_results.Text = msg;
                }
            }
            
        }

        #endregion

        #region support methods

        internal Color parseColor(string color_theme)
        {
            try
            {
                Color c = Color.FromKnownColor(KnownColor.White);
                if (color_theme.Length == 0) color_theme = "#86B3EE";
                {
                    if (color_theme[0] == '#')
                    {
                        try
                        {
                            int r = Int32.Parse(color_theme.Substring(1, 2), System.Globalization.NumberStyles.HexNumber);
                            int g = Int32.Parse(color_theme.Substring(3, 2), System.Globalization.NumberStyles.HexNumber);
                            int b = Int32.Parse(color_theme.Substring(5, 2), System.Globalization.NumberStyles.HexNumber);
                            c = Color.FromArgb(r, g, b);
                        }
                        catch (Exception ex)
                        {
                            throw new Exception("Error in color format: " + ex.Message);
                        }
                    }
                    else
                    {
                        string[] allcolours = Enum.GetNames(typeof(KnownColor));
                        Array colorsArray = Enum.GetValues(typeof(KnownColor));
                        KnownColor[] colours = new KnownColor[allcolours.Length];
                        Array.Copy(colorsArray, colours, colorsArray.Length);
                        for (int i = 0; i < allcolours.Length; i++)
                        {
                            if (allcolours[i].ToLower() == color_theme.ToLower())
                            {
                                c = Color.FromKnownColor(colours[i]);
                                break;
                            }
                        }
                    }
                }
                return c;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                {
                    throw new Exception("parseColor: " + ex.Message, ex.InnerException);
                }
                else
                {
                    throw new Exception("parseColor: " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Returns a list from the current site if the user has permission to access it, else returns null
        /// </summary>
        /// <param name="Guid">GUID of the list</param>
        /// <returns></returns>
        private SPList getListCurrentUser(Guid libGuid)
        {
            try
            {
                SPWeb thisWeb = SPContext.Current.Web;
                thisWeb.Lists.ListsForCurrentUser = true;
                SPListCollection splc = thisWeb.Lists;
                foreach (SPList list in splc)
                {
                    if (list.ID == libGuid)
                    {
                        return list;
                    }
                }
                return null;
            }
            catch (Exception ex)
            {
                throw new Exception("getListCurrentUser: " + ex.Message, ex.InnerException);
            }
        }

        private void populateLibShortUrls()
        {
            try
            {
                libShortUrls.Clear();
                foreach (string key in am_libs_sel.Keys)
                {
                    SPList aList;
                    Guid libGuid = new Guid((string)AMLibsSel[key]);
                    try
                    {
                        aList = getListCurrentUser(libGuid);
                        if (aList != null)
                        {
                            string durl = aList.DefaultViewUrl;
                            string purl = aList.ParentWebUrl;
                            int chop = purl.Length;
                            if (purl.LastIndexOf('/') != purl.Length - 1)
                            {
                                chop++;
                            }
                            string turl = durl.Substring(chop);
                            string libShortUrl = turl.Substring(0, turl.IndexOf('/')).ToLower();
                            libShortUrls.Add(libShortUrl);
                        }
                        else
                        {
                            throw new Exception("You do not have permissions on one or more lists configured for this web part");
                        }
                    }
                    catch (Exception ex)
                    {
                        throw new Exception("Error: " + ex.Message, ex.InnerException);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception("populateLibShortUrls: " + ex.Message, ex.InnerException);
            }
        }

        internal void UpdateColumnValues()
        {
            try
            {
                AMColVals.Clear();
                SPWeb thisWeb = SPContext.Current.Web;
                foreach (string key in AMLibsSel.Keys)
                {
                    Guid libGuid = new Guid((string)AMLibsSel[key]);                    
                    //SPList aList = thisWeb.Lists.GetList(libGuid, false); // This needs to be modified - user may not have access
                    SPList aList = getListCurrentUser(libGuid); // slower (probably), but null if user doesn't have access
                    if (aList != null)
                    {
                        if (show_debug)
                        {
                            string unFormatedXml = aList.Fields.SchemaXml;
                            debug_text = FormatXml(unFormatedXml);
                        }
                        List<string> fields = new List<string>();
                        foreach (string field in AMColsSel.Keys)
                        {
                            if (!fields.Contains(field))
                            {
                                fields.Add(field);
                            }
                        }
                        foreach (string field in AMSearchColsSel.Keys)
                        {
                            if (!fields.Contains(field))
                            {
                                fields.Add(field);
                            }
                        }
                        foreach (string field in fields)
                        {
                            if (!fieldTypes.ContainsKey(field))
                            {
                                if (aList.Fields.ContainsField(field))
                                {
                                    SPField aField = aList.Fields[field];
                                    /*
                                     * 0 = text (also date, choice & others that behave nicely under the ToString method)
                                     * 1 = lookup
                                     * 2 = person
                                     * 
                                     */
                                    int ftype = 0;
                                    if (aField.Type == SPFieldType.Lookup)
                                    {
                                        ftype = 1;
                                    }
                                    else if (aField.Type == SPFieldType.User)
                                    {
                                        ftype = 2;
                                    }
                                    fieldTypes[field] = ftype;
                                }
                            }
                        }
                        foreach (SPListItem anItem in aList.Items)
                        {
                            foreach (string field in AMColsSel.Keys)
                            {
                                if (AMColVals[field] == null)
                                {
                                    AMColVals[field] = new List<string>();
                                }
                                if (aList.Fields.ContainsField(field))
                                {
                                    SPField aField = aList.Fields[field];
                                    if (anItem[field] != null)
                                    {
                                        int ftype = (int)fieldTypes[field];
                                        string val = Field2String(field, ftype, anItem);
                                        addFieldValueIfNew(field, val);
                                    }
                                }
                                List<string> vals = AMColVals[field] as List<string>;
                                vals.Sort();
                                List<string> revVals = new List<string>();
                                for (int iString = vals.Count - 1; iString >= 0; iString--)
                                {
                                    revVals.Add(vals[iString]);
                                }
                                if (reverse_alpha)
                                {
                                    AMColVals[field] = revVals;
                                }
                                else
                                {
                                    AMColVals[field] = vals;
                                }
                            }
                        }
                    }
                }
                // need to update controls
                this.Controls.Clear();
                this.CreateChildControls();
            }
            catch (Exception ex)
            {
                throw new Exception("UpdateColumnValues: " + ex.Message, ex.InnerException);
            }
        }

        private void addFieldValueIfNew(string field, string val)
        {
            try
            {
                bool isFieldValListed = false;
                //CASE INSENSITIVITY MOD
                /*
                 * The value stored should honour the case of the first case
                 * value found (or the most "mixed-case" value), but the test
                 * should be case-insensitive.
                 * 
                 * Search filter also then needs to be case insensitive (done)
                 */
                List<string> cvlist = AMColVals[field] as List<string>;
                int startCount = cvlist.Count;
                for (int fcindex = 0; fcindex < cvlist.Count; fcindex++)
                {
                    string fcval = cvlist[fcindex];
                    string cival = val.ToLower();
                    if (fcval.ToLower() == cival)
                    {
                        isFieldValListed = true;
                        //Same field value - check case
                        if (val != fcval)
                        {
                            //differ, but only by case
                            /*
                             * Use highest scoring version:
                             * Max score = nchars-1
                             * all upper case = 1
                             * all lower case = 0
                             * Mixed case, score = nchars - no. upper case characters
                             * 
                             * So highest score is a mixed case with only 1 upper case char.
                             * All upper case except for 1 char scores equal with all lower case
                             * All upper case beats all lower case as it's probably an acronym
                             * 
                             */
                            int valScore = val.Length;
                            int fcvalScore = fcval.Length;
                            char[] cval = val.ToCharArray();
                            char[] cfcval = fcval.ToCharArray();
                            for (int ic = 0; ic < val.Length; ic++)
                            {
                                //ascii upper case = 65->91
                                if (((int)cval[ic] >= 65) && ((int)cval[ic] <= 91)) valScore--;
                                if (((int)cfcval[ic] >= 65) && ((int)cfcval[ic] <= 91)) fcvalScore--;
                            }
                            if (val == val.ToLower()) valScore = 0;
                            else if (val == val.ToUpper()) valScore = 1;
                            if (fcval == fcval.ToLower()) fcvalScore = 0;
                            else if (fcval == fcval.ToUpper()) fcvalScore = 1;
                            if (valScore > fcvalScore)
                            {
                                //Need to swap this string into the string list in place of existing
                                cvlist.RemoveAt(fcindex);
                                cvlist.Insert(fcindex, fcval);
                                //Now reinsert this modified list into the hash table:
                                if (cvlist.Count != startCount)
                                {
                                    throw new Exception("list of column values has changed size");
                                }
                                AMColVals[field] = cvlist;
                            }
                        }
                    }
                }
                if (!isFieldValListed) (AMColVals[field] as List<string>).Add(val);
                //previous code:
                //if (!(AMColVals[field] as List<string>).Contains(val))
                //{
                //    (AMColVals[field] as List<string>).Add(val);
                //}
            }
            catch (Exception ex)
            {
                throw new Exception("addFieldValueIfNew: " + ex.Message, ex.InnerException);
            }
        }

        private string Field2String(string col, int fieldType, SPListItem item)
        {
            try
            {
                // the return value of item[col] is an object. By using only "ToString" I may be
                // inadvertantly creating a valid column value where none exists.
                string val = item[col].ToString();
                char[] delims = new char[1];
                if (fieldType == 1)
                {
                    delims[0] = '#';
                    string bit = val.Split(delims)[1];
                    val = bit;
                }
                else if (fieldType == 2)
                {
                    SPFieldUserValue userFieldVal = new SPFieldUserValue(item.Web, item[col] as string);
                    if (userFieldVal.User != null)
                    {
                        val = userFieldVal.User.Name;
                    }
                    else
                    {
                        val = "null";
                    }
                }
                if (col.ToLower() == val.ToLower())
                {
                    _message=_message+"<br>Warning: - column value found equal to column name. "
                    + "Please check if this is correct or indicative of a bug" + "<br>";
                }
                return val;
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                {
                    throw new Exception("Field2String: " + ex.Message, ex.InnerException);
                }
                else
                {
                    throw new Exception("Field2String: " + ex.Message);
                }
            }
        }

        /// <summary>
        /// Returns formatted xml string (indent and newlines) from unformatted XML
        /// string for display in eg textboxes.
        /// </summary>
        /// <param name="sUnformattedXml">Unformatted xml string.</param>
        /// <returns>Formatted xml string and any exceptions that occur.</returns>
        private string FormatXml(string sUnformattedXml)
        {
            //load unformatted xml into a dom
            XmlDocument xd = new XmlDocument();
            xd.LoadXml(sUnformattedXml);

            //will hold formatted xml
            StringBuilder sb = new StringBuilder();

            //pumps the formatted xml into the StringBuilder above
            StringWriter sw = new StringWriter(sb);

            //does the formatting
            XmlTextWriter xtw = null;

            try
            {
                //point the xtw at the StringWriter
                xtw = new XmlTextWriter(sw);

                //we want the output formatted
                xtw.Formatting = Formatting.Indented;

                //get the dom to dump its contents into the xtw
                xd.WriteTo(xtw);
            }
            finally
            {
                //clean up even if error
                if (xtw != null)
                    xtw.Close();
            }

            //return the formatted xml
            return sb.ToString();
        }
        /*
    * The following method should be obsoleted by the AMSearchResult class         
    */
        /// <summary>
        /// Find and highlight in bold any occurances of a string in a larger string
        /// </summary>
        /// <param name="foundText">The text in which to find occurances of the query string</param>
        /// <param name="query">The string to look for, and highlight if found, in the foundText</param>
        /// <returns>html formatted text with query string in bold</returns>
        private string highlightText(string foundText, string query)
        {
            try
            {
                string retString = "";
                int foundLen = foundText.Length;
                int queryLen = query.Length;
                List<string> subs = new List<string>();
                for (int i = 0; i < foundLen - queryLen; i++)
                {
                    string bit = foundText.Substring(i, queryLen);
                    if (bit.ToLower() == query.ToLower())
                    {
                        if (!subs.Contains(bit))
                        {
                            subs.Add(bit);
                        }
                    }
                }
                foreach (string bit in subs)
                {
                    string nbit = "<b>" + bit + "</b>";
                    retString = foundText.Replace(bit, nbit);
                    foundText = retString;
                }
                if (subs.Count > 0)
                {
                    return foundText + "<br>";
                }
                else
                {
                    return "";
                }
            }
            catch (Exception ex)
            {
                throw new Exception("highlightText: " + ex.Message, ex.InnerException);
            }
        }
        #endregion

        #region editor part methods
        //
        // Note: no need to implement IWebEditable, as the EditorPart that our AMFilteredSearchEditor derives from already does so.
        //
        public override EditorPartCollection CreateEditorParts()
        {
            try
            {
                List<EditorPart> editors = new List<EditorPart>();
                EditorPart editor = new AMFilteredSearchEditor(this.ID);
                editor.Title = "Filtered Search Web Part";
                editor.Description = "Select libraries to include in search, and column names to restrict results by";
                editors.Add(editor);
                return new EditorPartCollection(editors);
            }
            catch (Exception ex)
            {
                throw new Exception("CreateEditorParts: " + ex.Message, ex.InnerException);
            }
        }

        //object IWebEditable.WebBrowsableObject
        public override object WebBrowsableObject
        {
            //barely implemented...
            get { return this; }
        }
        #endregion
    }
}
